package JET.physics;

import JET.geom.Sec2d;
import JET.geom.Sec2dIntersectionInfo;
import JET.geom.Vec2d;
import JET.geom.Vec3d;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Collection;

/**
 *
 * @author 057Ry
 */
public final class Border
{
    private ArrayList<Wall> wallList;

    private boolean properlyBuilt = false;
    private boolean isCCW = false;

    private final Vec2d massNorm = new Vec2d();

    public Border() {
        wallList = new ArrayList<Wall>();
    }

    public boolean isProperlyBuilt() {
        return properlyBuilt;
    }

    public void setProperlyBuilt(boolean properlyBuilt) {
        this.properlyBuilt = properlyBuilt;
    }

    public void addWall(Wall wall)
    {
        wallList.add( wall );
        checkIfBorderIsProper();
    }
    //public void addWalls(ArrayList<Wall> wallListToAdd)
    public void addWalls(Collection<Wall> wallListToAdd)
    {
        wallList.addAll(wallListToAdd);
        checkIfBorderIsProper();
    }
    public void removeWalls2dThatUsesMass2d(Mass mass2d)
    {
        for(int i =0; i<wallList.size(); i++)
            if( wallList.get(i).getM1().equals(mass2d) || wallList.get(i).getM2().equals(mass2d) )
                removeWall( wallList.get(i--) );
                //StaticActions.removeWallFromSpringBody( wallList.get(i--) );
    }
    public void removeWall(Wall wallToRemove)
    {
        for(Wall wall : wallList)
            if ( !wall.equals(wallToRemove) )
            {
                if( wall.next != null && wall.next.equals(wallToRemove) )
                    wall.next = null;
                if( wall.prev != null && wall.prev.equals(wallToRemove) )
                    wall.prev = null;
            }

        Collection<Wall> wallCol = getWallListSync();
        synchronized(wallCol) {
            wallCol.remove(wallToRemove);
        }
        checkIfBorderIsProper();
    }
    public synchronized ArrayList<Wall> getWallList() {
        return wallList;
    }

    public Collection<Wall> getWallListSync() {
        return Collections.synchronizedList(wallList);
    }
    
    public Wall getWall(int n)
    {
        return wallList.get(n);
    }
    public Wall getWallWhichV1Is(Mass mass)
    {
        for(int i =0; i<wallList.size(); i++)
            if( wallList.get(i).getM1().equals(mass) )
                return wallList.get(i);
        return null;
    }
    public ArrayList<Mass> getSortedMassList()
    {
        if ( wallList.size() > 0 )
        {
            ArrayList<Mass> sml = new ArrayList<Mass>( wallList.size()-1 );
            for(int i=0; i<wallList.size(); i++)
            {
                Wall wall = wallList.get(i);
                if( wall.getM1() instanceof Mass )
                {
                    Mass mass = (Mass)wall.getM1();
                    sml.add( mass );
                }
                else
                {
                    System.out.println(" TODO : rozszerzyc brzeg o mozliwosc oblsugi innych obiektow niz Mass2d...");
                    return null;
                }
            }
            return sml;
        }
        return null;
    }
    public boolean checkIfBorderIsProper()
    {
        if ( this.sortWalls() && this.borderBackClosed() )
        {
            if ( isCounterClockwise() )
            {
                Collection<Wall> wallCol = Collections.synchronizedList(wallList);
                synchronized(wallCol) {
                    for(Wall wall : wallCol) {
                        Mass tmpVertex = wall.getM1();
                        synchronized(wall) {
                            wall.m1 = wall.m2;
                            wall.m2 = tmpVertex;
                        }
                    }
                }
            }
            properlyBuilt = true;
            return true;
        }
        else
        {
            properlyBuilt = false;
            return false;
        }
    }
    /**
     * Sprawdza czy lamana tworzaca brzeg jest zamknieta.
     * Metoda sprawdza jedynie czy pierwsza (zerowa) sciana brzegu posiada
     * przypisana sciane poprzednia.
     * Metoda moze zwrocic wartosc <code>true</code> w wypadku gdy lamana nie jest
     * zamknieta - np wtedy gdy brzeg posiada 2 siciany, ale ktorykolwiek z wierzcholkow drugiej sciany
     * jest tez wierzcholkiem sciany pierwszej.
     * Aby miec pewnosc ze brzeg jest lamana zamknieta nalezy wziac pod uwage rowniez
     * wynik dzialania metody <code>sortWalls()</code>.
     *
     * @return czy pierwsza(zerowa) sciana brzegu ma poprzednika.
     */
    public boolean borderBackClosed()
    {
        if ( wallList.size() > 0 )
        {
            Wall wallZero = wallList.get(0);
            Wall prevWall;
            Mass wallZeroV1 = wallZero.getM1();

            // jezeli ktoras sciana ma v2.equalTo( wallZeroV1 ) to znaczy ze sciana zero ma poprzednika
            if ( (prevWall=getWallWithM2EqualTo(wallList, wallZero, wallZeroV1)) == null)
                    return false;
            else
            {
                wallZero.setPrevNeighbour(prevWall);
                prevWall.setNextNeighbour(wallZero);
                return true;
            }
        }
        else
            return false;
    }

    /**
     * Sortuje sciany brzegu zgodnie z kierunkiem utworzenia sciany zerowej.
     * Jezeli sciany nie tworza pojedynczej lamanej, metoda nie potrafi ich posortowac i
     * przerywa swoje dzialanie wew petli for zwracajc <code>false</code>.
     * Jezeli sciany tworza pojedyncza lamana, metoda po posortowaniu scian zwraca wartosc
     * <code>true</code>.
     *
     * To ze metoda zwraca wartosc <code>true</code> nie oznacza ze posortowane sciany
     * moga tworzyc prawidlowy brzeg - lamana ze scian powinna byc zamknieta, wiec aby
     * sie upewnic czy brzeg jest prawidlowo zbudowany, trzeba jeszcze sprawdzic zamkniecie
     * brzegu metoda <code>boolean borderBackClosed()</code>.
     *
     * @return czy udalo sie posortowac sciany brzegu.
     */
    private boolean sortWalls()
    {
        ArrayList<Wall> tmpList = new ArrayList<Wall>( wallList.size() );
        tmpList.addAll( wallList );

        boolean nextWallInverted = false;
        Mass wallV;

        for( int i=0; i<tmpList.size()-1; i++ )
        {
            Wall wall = tmpList.get(i);
            if ( nextWallInverted )
                wallV = wall.getM1();
            else
                wallV = wall.getM2();

            Wall nextWall = getWallWithM1EqualTo(wallList, wall, wallV );
            nextWallInverted = false;
            if ( nextWall == null )
            {
                nextWall = getWallWithM2EqualTo(wallList, wall, wallV );
                nextWallInverted = true;
            }

            // wyjscie jezeli nie da sie zbudowac brzegu z aktualnie posiadanych scian
            if ( nextWall == null){
                return false;
            }

            tmpList.remove( nextWall );
            tmpList.add(i+1, nextWall );

            tmpList.get(i).setNextNeighbour( nextWall );
            tmpList.get(i+1).setPrevNeighbour( wall );

            if ( nextWallInverted ){
                nextWall.invert();
                nextWallInverted = false;
            }
        }

        boolean sorted = true;
        for(Wall wall : wallList)
            if( !tmpList.contains(wall) ) {
                sorted = false;
                tmpList.add(wall);
            }
        
        wallList = tmpList;

        return sorted;
    }
    private Wall getWallWithM1EqualTo(ArrayList<Wall> list, Wall notWantedWall,Mass l)
    {
        for(Wall wall : list)
            if( !wall.equals(notWantedWall) && wall.getM1().equals(l) )
                return wall;

        return null;
    }
    private Wall getWallWithM2EqualTo(ArrayList<Wall> list, Wall notWantedWall, Mass l)
    {
        for(Wall wall : list)
            if( !wall.equals(notWantedWall) && wall.getM2().equals(l) )
                return wall;

        return null;
    }
    /**
     * Zwraca liste punktow przeciecia scian brzegu z pozioma prosta <code>scanLine = vec.getY()</code>
     *
     * @param vec definiuje polozenie prostej <code>scanLine</code> (pod uwage brana jest jedynie wsp. Y wektora <code>vec</code>).
     * @return lista punktow przeciecia scian brzegu z prosta poziomą <code>y = vec.getY()</code>.
     */
    public ArrayList<Vec2d> getIntersectionList(Vec2d vec)
    {
        Sec2d scanLine = new Sec2d( new Vec2d(Double.MIN_VALUE, vec.getY()), new Vec2d(Double.MAX_VALUE, vec.getY()) ) ;
        ArrayList<Vec2d> list = new ArrayList<Vec2d>(wallList.size());
        for(Wall wall : wallList)
            if ( wall.getM1().getPosition().getY() == wall.getM2().getPosition().getY() )
            {
                if ( vec.getY() == wall.getM1().getPosition().getY() )
                {
                    list.add( new Vec2d(wall.getM1().getPosition().getX(), vec.getY()) );
                    list.add( new Vec2d(wall.getM2().getPosition().getX(), vec.getY()) );
                }
            }
            else if( (wall.getM1().getPosition().getY()<=vec.getY() && wall.getM2().getPosition().getY()>=vec.getY() ) ||
                (wall.getM1().getPosition().getY()>=vec.getY() && wall.getM2().getPosition().getY()<=vec.getY() ) )
            {
                Vec2d intrscVec = wall.getSector().getIntersectionInfo(scanLine).getVec2d();
                list.add( intrscVec );
            }

        if ( list.size() > 0)
            return list;
        else
            return null;
    }
    
//    public boolean contains(Vec2d vec)
//    {
//        Sec2d scanLine = new Sec2d( new Vec2d(-1000000, vec.getY()), new Vec2d(1000000, vec.getY()) ) ;
//        ArrayList<Vec2d> list = new ArrayList<Vec2d>(wallList.size());
//        int stop = 0;
//        for(Wall wall : wallList)
//            if( (wall.getM1().getPosition().getY()<=vec.getY() && wall.getM2().getPosition().getY()>=vec.getY() ) ||
//                (wall.getM1().getPosition().getY()>=vec.getY() && wall.getM2().getPosition().getY()<=vec.getY() ) )
//            {
//                if (wall==null || wall.getSector()==null || scanLine==null || wall.getSector().getIntersectionInfo(scanLine)==null) {
//                    return false;
//                    //return true;
//                }
//                Vec2d intrscVec = wall.getSector().getIntersectionInfo(scanLine).getVec2d();
//                if (intrscVec != null )
//                    list.add( intrscVec );
//            }
//
//        if ( list.size() > 0)
//        {
//            Collections.sort( (List)list, new Vec2dComparator() );
//            int count = 0;
//            for(Vec2d v : list)
//                if( v.getX() < vec.getX() ) count++;
//                else break;
//
//            if ( count % 2 != 0) // jezeli count nieparzyste
//                return true;
//            else
//                return false;
//        }
//
//        return false;
//    }
    public boolean contains(Vec2d pt) {
        Vec2d endPt = new Vec2d(Double.MAX_VALUE, pt.y);

        // line we are testing against goes from pt -> endPt.
        boolean inside = false;

        for (Wall w : wallList)
        {
            Vec2d edgeSt = w.getM1().getPosition();
            Vec2d edgeEnd = w.getM2().getPosition();

            // perform check now...
            if (((edgeSt.y <= pt.y) && (edgeEnd.y > pt.y)) || ((edgeSt.y > pt.y) && (edgeEnd.y <= pt.y)))
            {
                // this line crosses the test line at some point... does it do so within our test range?
                //float slope = mEdgeInfo[i].slope; //(edgeEnd.X - edgeSt.X) / (edgeEnd.Y - edgeSt.Y);
                double slope = (edgeEnd.x - edgeSt.x) / (edgeEnd.y - edgeSt.y);

                //float hitX = edgeSt.X + ((pt.Y - edgeSt.Y) * slope);
                double hitX = edgeSt.x + ((pt.y - edgeSt.y) * slope);

                if ((hitX >= pt.x) && (hitX <= endPt.x))
                    inside = !inside;
            }

        }

        return inside;
    }

    /**
     * Zwraca liste informcji o punktach przeciecia brzegu z odcinkiem.
     * Lista informacji o punktach przeciecia, dla kazdego przeciecia zawiera informacje
     * o punkcie przeciecia i parametrze <code>t</code> (przedzial t [0,1]) , mowiacym jak
     * daleko od pierwszego wierzcholka odcinka lezy dany punkt przeciecia.
     *
     * @param sec odcinek, dla ktorego poszukujemy punktow przeciecia
     * @return lista informcji o punktach przeciecia brzegu z odcinkiem.
     */
    public ArrayList<Sec2dIntersectionInfo> getIntersectionInfoList(Sec2d sec)
    {
        ArrayList<Sec2dIntersectionInfo> list = new ArrayList<Sec2dIntersectionInfo>();
        Sec2dIntersectionInfo s2dii;

        for(Wall wall : wallList)
            if( (s2dii=wall.getSector().getIntersectionInfo(sec)) != null )
                list.add(s2dii);

        if( list.size()>0 )
        {
            Collections.sort(list, new Sec2dIntersectionInfoComparator());
            return list;
        }

        return null;
    }
    /**
     * Zwraca informcje o punkcie przeciecia pierwszej napotkanej sciany brzegu z odcinkiem.
     * Iformacjia o punktcie przeciecia zawiera informacje o polozeniu przeciecia oraz
     * parametrze <code>t</code> (przedzial t [0,1]) , mowiacym jak
     * daleko od pierwszego wierzcholka odcinka lezy punkt przeciecia.
     *
     * @param sec odcinek, dla ktorego poszukujemy punktu przeciecia
     * @return informcja o punktcie przecieci pierwszej napotkanej sciany brzegu z odcinkiem.
     */
    public Sec2dIntersectionInfo getIntersectionInfo(Sec2d sec)
    {
        ArrayList<Sec2dIntersectionInfo> list = new ArrayList<Sec2dIntersectionInfo>();
        Sec2dIntersectionInfo s2dii;

        for(Wall wall : wallList)
            if( (s2dii=wall.getSector().getIntersectionInfo(sec)) != null )
                list.add(s2dii);

        if( list.size()>0 )
        {
            Collections.sort(list, new Sec2dIntersectionInfoComparator());
            return list.get(0);
        }

        return null;
    }

    public ArrayList<Wall> getWallsIntersectingWith(Sec2d sec)
    {
        ArrayList<Wall> list = new ArrayList<Wall>();

        for(Wall wall : wallList)
            if( wall.intersectsWith(sec) )
                list.add(wall);

        if( list.size()>0 )
            return list;
        else
            return null;
    }
    public int getWallsCount()
    {
        return wallList.size();
    }

    /**
     * Calculater border orientation (CW or CCW).
     */
    public void calculateOrientation()
    {
        Vec3d testVec = new Vec3d();
        for(int i=0; i<wallList.size()-1; i++)
        {
            Wall wall1 = wallList.get(i);
            Wall wall2 = wallList.get(i+1);
            testVec.add( Vec2d.cross(wall1.getVector(), wall2.getVector()) );
        }
        if ( testVec.getZ() >=0 ) isCCW = true;
        else                     isCCW = false;
    }
    /**
     * Tests if the border is counter-clockwise.
     * @return true if polygon is counter-clockwise.
     */
    public boolean isCounterClockwise()
    {
        return isCCW;
    }

    public double getClosestPointOnEdgeSquared(Vec2d pt, Wall w, Vec2d hitPt, Vec2d norm, double[] edgeD) {
        hitPt.become(Vec2d.zero);
        norm.become(Vec2d.zero);

        edgeD[0] = 0.0;
        double dist = 0.0;

        Vec2d ptA = w.getM1().getPosition();
        Vec2d ptB = w.getM2().getPosition();

        Vec2d toP = pt.getSubbed(ptA);
        Vec2d E = w.getVector().getNormalized();

        double edgeLength = w.getVector().getMagnitude();

        Vec2d n = E.getPerp();

        double x = toP.dot(E);

        if (x<=0.0)
        {
            // x is outside the line segment, distance is from pt to ptA.
            dist = pt.getSubbed(ptA).getMagnitudeSquared();
            hitPt.become(ptA);
            edgeD[0] = 0.0;
            norm.become(n);
        }
        else if (x>=edgeLength)
        {
            // x is outside of the line segment, distance is from pt to ptB.
            dist = pt.getSubbed(ptB).getMagnitudeSquared();
            hitPt.become(ptB);
            edgeD[0] = 1.0;
            norm.become(n);
        }
        else
        {
            // point lies somewhere on the line segment.
            //dist = toP.crossProduct(E);
            dist = Vec2d.crossZ(toP, E);
            dist = (dist * dist);
            //hitPt = ptA + (E * x);
            hitPt.become(ptA.getAdded(E.getScaled(x)));
            edgeD[0] = x / edgeLength;
            norm.become(n);
        }

        return dist;
    }

    public Vec2d getMassNormal(int n)
    {
        int prevWall = n>0 ? n-1 :wallList.size()-1;

        Wall wall1 = wallList.get(n);
        Wall wall2 = wallList.get(prevWall);

        massNorm.become( wall1.getVector().getNormalized() );
        massNorm.add( wall2.getVector().getNormalized() );
        massNorm.makePerp();

        return massNorm;
    }
    
    public Vec2d getMassNormal(Mass mass)
    {
        Wall wall = getWallWhichV1Is(mass);
        int n = wallList.indexOf(wall);
        
        return getMassNormal(n);
    }
    /**
     *
     * @author 057Ry
     */
    class Vec2dComparator implements Comparator
    {
        public int compare(Object o1, Object o2)
        {
            Vec2d vec1 = (Vec2d)o1;
            Vec2d vec2 = (Vec2d)o2;

            if ( vec1.getX() < vec2.getX() )
                return -1;
            else if ( vec1.getX() > vec2.getX() )
                return 1;
            else
                return 0;
        }
    }
    class Sec2dIntersectionInfoComparator implements Comparator
    {

        public int compare(Object o1, Object o2) {
            Sec2dIntersectionInfo s2dii1 = (Sec2dIntersectionInfo)o1;
            Sec2dIntersectionInfo s2dii2 = (Sec2dIntersectionInfo)o2;

            if ( s2dii1.getT() < s2dii2.getT() )
                return -1;
            else if ( s2dii1.getT() > s2dii2.getT() )
                return 1;
            else
                return 0;
        }
    }
}
